%%%-------------------------------------------------------------------
%%% File    : ewgi_mochiweb.erl
%%% Authors : Filippo Pacini <pacini@sgconsulting.it>
%%%           Hunter Morris <huntermorris@gmail.com>
%%% License :
%%% The contents of this file are subject to the Mozilla Public
%%% License Version 1.1 (the "License"); you may not use this file
%%% except in compliance with the License. You may obtain a copy of
%%% the License at http://www.mozilla.org/MPL/
%%%
%%% Software distributed under the License is distributed on an "AS IS"
%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%%% the License for the specific language governing rights and
%%% limitations under the License.
%%% The Initial Developer of the Original Code is S.G. Consulting
%%% srl. Portions created by S.G. Consulting s.r.l. are Copyright (C)
%%% 2007 S.G. Consulting srl. All Rights Reserved.
%%%
%%% @doc 
%%% <p>Sample implementation of a mochiweb server gateway.</p>
%%%
%%% @end
%%%
%%% Created : 12 Oct 2007 by Filippo Pacini <pacini@sgconsulting.it>
%%%-------------------------------------------------------------------
-module(ewgi_mochiweb, [Appl]).

%% API
%% ewgi callbacks
-export([setup_environ/1,
         start_response/1,
         run/1,
         write/2,
         end_response/1
        ]).

-include("../include/ewgi.hrl").

-define(ewgi2m(Err, Hdrs), {element(1, Err), Hdrs, element(2, Err)}).

%%====================================================================
%% API
%%====================================================================

%%====================================================================
%% ewgi_server callbacks
%%====================================================================
run(Req) ->
    Env = (catch setup_environ(Req)),
    case Env of
        {'EXIT', Reason} ->
            error_logger:error_report(Reason),
            Req:respond(?ewgi2m(?BAD_REQ, []));
        {error, Reason} ->
            error_logger:error_report(Reason),
            Req:respond(?ewgi2m(?BAD_REQ, []));
        Env ->
            error_logger:info_report(Env),
            OutBuf = ewgi_buffer:start(),
            Result = (catch process_application(Env, OutBuf)),
            handle_result(Req, OutBuf, Result)
    end.

handle_result(Req, _, {'EXIT', Reason}) ->
    error_logger:error_report(Reason),
    Req:respond(?ewgi2m(?INTERNAL_SERVER_ERROR, []));
handle_result(Req, _, {error, Reason}) ->
    error_logger:error_report(Reason),
    Req:respond(?ewgi2m(?INTERNAL_SERVER_ERROR, []));
handle_result(Req, _, not_found) ->
    Req:not_found();
%% Chunked response if a nullary function is returned
handle_result(Req, OutBuf, F) when is_function(F, 0) ->
    {Code, ResponseHeaders, Body} = end_response(OutBuf),
    MochiResp = Req:respond({Code, ResponseHeaders, chunked}),
    case Body of
        [] -> ok;
        Body -> MochiResp:write_chunk(Body)
    end,
    handle_stream_result(MochiResp, F());
handle_result(Req, OutBuf, Chunk) when is_binary(Chunk); is_list(Chunk) ->
    write(OutBuf, Chunk),
    Req:respond(end_response(OutBuf));
handle_result(Req, _, _) ->
    Req:respond(?ewgi2m(?INTERNAL_SERVER_ERROR, [])).

handle_stream_result(R, {}) ->
    R:write_chunk([]);
handle_stream_result(R, {H, T}) when is_function(T, 0) ->
    R:write_chunk(H),
    handle_stream_result(R, T());
handle_stream_result(R, {[], T}) when is_function(T, 0) ->
    handle_stream_result(R, T());
handle_stream_result(R, {<<>>, T}) when is_function(T, 0) ->
    handle_stream_result(R, T()).

process_application({'EXIT', Reason}, _) ->
    {error, Reason};
process_application(Env, OutBuf) when is_list(Appl) ->
    Path = ewgi_api:path_info(Env),
    process_mount_application(Env, OutBuf, Path, find_mount(Appl, Path));
process_application(Env, OutBuf) ->
    ewgi_application:run(Appl, Env, start_response(OutBuf)).

process_mount_application(_, _, _, {not_found, _}) ->
    not_found;
process_mount_application(Env, OutBuf, Path, {MountPoint, Application}) ->
    Env1 = [proplists:property({"SCRIPT_NAME", MountPoint})|Env],
    Env2 = case Path of
               "*" ->
                   ewgi_api:set_value("PATH_INFO", "*", Env1);
               _ ->
                   ewgi_api:set_value("PATH_INFO", string:substr(Path, length(MountPoint) + 1), Env1)
           end,
    ewgi_application:run(Application, Env2, start_response(OutBuf)).

find_mount([], _) ->
    {not_found, fun (_, _) -> not_found end};
find_mount(Mounts, "*") ->
    lists:last(Mounts);
find_mount([{Point, _}=M|_], Path) when Point =:= Path ->
    M;
find_mount([{Point, _}=M|T], Path) ->
    case string:str(Path, Point ++ "/") of
        1 ->
            M;
        _ ->
            find_mount(T, Path)
    end.

setup_environ(Req) ->
    parse_arg(Req).

start_response(OutBuf) ->
    Resp = fun(Status, Headers) ->
                   ewgi_buffer:append(OutBuf, Status),
                   ewgi_buffer:append(OutBuf, Headers),
                   fun(Data) ->
                           ewgi_buffer:append(OutBuf, Data)
                   end
           end,
    Resp.

write(OutBuf, Result) ->
    %% if out buffer is empty fill it with default values
    case ewgi_buffer:is_empty(OutBuf) of
        true ->
            ewgi_buffer:append(OutBuf, ?OK),
            ewgi_buffer:append(OutBuf, [{"Content-Type", "text/html"}]);
        false -> void
    end,
    ewgi_buffer:append(OutBuf, Result).

end_response(OutBuf) ->
    %% extract status and content type 
    %% and return response as {Status, Headers, Body}
    %% e.g. {200, [{"Content-Type", "text/html"}], <<"Body">>}
    BufferList = ewgi_buffer:close(OutBuf),
    extract_triple(BufferList).

extract_triple([{Status, _Msg}, Headers|Body]) ->
    {Status, Headers, Body};
extract_triple([{Status, _Msg}, Headers]) ->
    {Status, Headers, []};
extract_triple([]) ->
    {500, [], []}.

%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
parse_arg(Req) ->
    Hdrs = parse_headers(Req),
    lists:foldl(fun({El, ElName}, PList) ->
                        V = parse_element(El, Req),
                        case V of
                            undefined -> PList;
                            V ->
                                NewEl = proplists:property({ElName, V}),
                                [NewEl|PList]
                        end
                end, Hdrs, ?EWGI_ENV_DATA).

parse_element(server_sw, _Req) ->
    "MochiWeb";

parse_element(gw_interface, _Req) ->
    "EWGI/1.0";

parse_element(server_name, Req) ->
    HostPort = Req:get_header_value(host),
    case HostPort of
        HostPort when is_list(HostPort) ->
            hd(string:tokens(HostPort, ":"));
        HostPort -> HostPort
    end;

parse_element(server_port, Req) ->
    HostPort = Req:get_header_value(host),
    case HostPort of
        HostPort when is_list(HostPort) ->
            case length(HostPort) of
                2 -> lists:nth(2, HostPort);
                _ -> undefined
            end;
        _ ->
            undefined
    end;

parse_element(server_protocol, Req) ->
    {Maj, Min} = Req:get(version),
    lists:flatten(io_lib:format("HTTP/~b.~b", [Maj, Min]));

parse_element(method, Req) ->
    Req:get(method);

parse_element(path_info, Req) ->
    RawPath = Req:get(raw_path),
    case RawPath of
        RawPath when RawPath =:= '*' ->
            "*";
        RawPath ->
            {Path, _, _} = mochiweb_util:urlsplit_path(RawPath),
            ewgi_api:unquote_path(Path)
    end;

%% Used to be:
%% filename:dirname(filename:dirname(code:which(Appl)))++Req:get(path); The
%% problem here is that the application only has a function which acts as the
%% entry point to the application.
parse_element(path_translated, _Req) ->
    undefined;

%% Default value is empty string. If mount points are used, SCRIPT_NAME
%% becomes the mount point.
parse_element(script_name, _Req) ->
    [];

parse_element(qs, Req) ->
    RawPath = Req:get(raw_path),
    case RawPath of
        RawPath when RawPath =:= '*' ->
            undefined;
        RawPath ->
            {_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath),
            QueryString
    end;

parse_element(content_type, Req) ->
    Req:get_header_value("content-type");

parse_element(content_length, Req) ->
    case Req:get_header_value("content-length") of
        undefined -> undefined;
        Length when is_integer(Length) ->
            Length;
        Length when is_list(Length) ->
            list_to_integer(Length)
    end;

parse_element(remote_host, _Req) ->
    undefined;

parse_element(remote_addr, Req) ->
    Req:get(peer);

parse_element(auth_type, _Req) ->
    undefined;

parse_element(remote_user, _Req) ->
    undefined;

parse_element(remote_ident, _Req) ->
    undefined;

parse_element(read_input, Req) ->
    F = fun(Callback, Length) ->
                case Req:get_header_value("expect") of
                    "100-continue" ->
                        Req:start_raw_response({100, gb_trees:empty()});
                    _Else ->
                        ok
                end,
                read_input(Callback, Length, Req)
        end,
    F;

parse_element(write_error, Req) ->
    F = fun(Msg) ->
                write_error(Msg, Req)
        end,
    F;

parse_element(ewgi_version, _Req) ->
    {1, 0};

parse_element(ewgi_data, _Req) ->
    [];

%% https?
parse_element(url_scheme, _Req) ->
    "http".

parse_headers(Req) ->
    Hdrs = Req:get(headers),
    lists:foldl(fun(Pair, Acc) ->
                        {K1, V1} = ewgi_api:normalize_header(Pair),
                        %% Don't duplicate content-length and content-type
                        case K1 of
                            "CONTENT_LENGTH" ->
                                Acc;
                            "CONTENT_TYPE" ->
                                Acc;
                            K1 ->
                                [{lists:append(["HTTP_", K1]), V1}|Acc]
                        end
                end,
                [],
                mochiweb_headers:to_list(Hdrs)).


%% No chunk size specified, so use default
read_input(Callback, Length, Req) when is_integer(Length) ->
    read_input(Callback, {Length, ?DEFAULT_CHUNKSIZE}, Req);

%% Final callback after entire input has been read
read_input(Callback, {Length, _ChunkSz}, _Req) when is_function(Callback), Length =< 0 ->
    Callback(eof);

%% Continue reading and calling back with each chunk of data
read_input(Callback, {Length, ChunkSz}, Req) when is_function(Callback) ->
    Bin = recv_input(Req, Length, ChunkSz),
    Rem = Length - size(Bin),
    NewCallback = Callback({data, Bin}),
    read_input(NewCallback, {Rem, ChunkSz}, Req).

%% Read either Length bytes or ChunkSz, whichever is smaller
recv_input(Req, Length, ChunkSz) when Length > 0, Length < ChunkSz ->
    Req:recv(Length);
recv_input(Req, _, ChunkSz) ->
    Req:recv(ChunkSz).

%% Write errors to error_logger
write_error(Msg, _Req) ->
    error_logger:error_report(Msg).
